/**
 * 
 */
package components.memory;

import java.util.Arrays;

import components.Util;
import components.exceptions.BinaryToIntException;
import components.exceptions.IntToBinaryException;
import components.exceptions.SubArrayException;

/**
 * 
 * In order to keep the whole system is using the same memory instance, the
 * memory class is a singleton. If you want to get an instance of Memory, call
 * {@code	Memory mem = Memory.getInstance();}.
 * 
 * This class in charge of set and get function of it's content.
 * 
 * @author Chz
 * 
 * 
 */
public class Memory {

	public static final int WORDLENGTH = 16;
	private Word[][] content = new Word[8192][2];
	private int used = 0; // used for count memory usage
	/**
	 * this class must be singleton so that we can synchronize all the data in
	 * our system
	 */
	private static Memory self;

	/**
	 * an inner class, used to address the memory. Shouldn't be accessed from
	 * outside, no one should know this class except {@link Memory} itself.
	 * 
	 * @author Chz
	 */
	private class MemAddr {
		int bank = 0; // from 0 - 1, store the bank information
		int row; // from 0 - 8191
		boolean byteOrWord = false;
		int byteAddress = 0;

		/**
		 * This constructor accept a {@link Word} type memory address, decode
		 * it, and store the decoded memory address into it's instance.
		 * 
		 * @param address
		 */
		MemAddr(int[] addr) {
			super();
			if (addr.length < 16) {
				try {
					row = Util.binaryToInt(addr);
			//		bank = addr[addr.length];
				} catch (BinaryToIntException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {

				@SuppressWarnings("unused")
				int[] tmp = new int[13]; // the tmp array is used to store the
											// address bits, totally 13 bits.
				// 0th bit indicates whether to address byte or not.
				// if not, address a word.
				if (addr[0] == 0)
					byteOrWord = false;
				else
					byteOrWord = true;

				// 1st bit indicates which byte to address if addr[0] is set.
				// if addr[0] is not set, this bit would be ignored.
				if (addr[1] == 0)
					byteAddress = 0;
				else
					byteAddress = 1;

				// get the addr[2-14] (13 bits) as row address

				try {
					tmp = Util.subArray(addr, 2, 14);
					row = Util.binaryToInt(tmp);
				} catch (SubArrayException e) {
					System.err.println("get memory address error.");
					e.printStackTrace();
				} catch (BinaryToIntException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				// addr[15] is i bank
				bank = addr[15];
			}

		}

	}

	/***
	 * and inner class, used to manage the content of memory. Shouldn't be
	 * accessed from outside, no one should know this class except
	 * {@link Memory} itself.
	 * 
	 * @author Chz
	 * 
	 */
	private class Word {

		int[] content = null;

		/***
		 * Constructor and initializer.
		 * 
		 * @param content
		 */
		Word(int[] content) {
			int[] tmpArray = new int[16];
			for (int i = 0; i < 16; i++)
				tmpArray[i] = content[i];

			this.content = tmpArray;

		}

		int[] toIntArray() {
			return content;
		}

		// return content[0-7]
		int[] getFirstByte() {
			int[] ret = new int[8];
			for (int i = 0; i < 7; ++i)
				ret[i] = content[i];

			return ret;
		}

		// return content[8-15]
		int[] getSecondByte() {
			int[] ret = new int[8];
			for (int i = 0; i < 7; ++i)
				ret[i] = content[8 + i];

			return ret;
		}

		protected Word clone() {

			int[] tmpArray = new int[16];
			for (int i = 0; i < 16; i++)
				tmpArray[i] = content[i];

			return new Word(tmpArray);

		}

		void setContent(int[] content) {
			int[] tmpArray = new int[16];
			for (int i = 0; i < 16; i++)
				tmpArray[i] = content[i];

			this.content = tmpArray;
		}
	}

	private Memory() {

	}

	/***
	 * @return and initialize (if necessary) the singleton instance.
	 */
	public static Memory getInstance() {
		if (self == null)
			self = new Memory();

		return self;
	}

	/***
	 * Accept input addr in int[] type, get corresponding content in the memory
	 * in int[] type.
	 * 
	 * @param addr
	 * @return content at memory location addr
	 */
	public int[] getContent(int[] addr) {
		MemAddr ma = new MemAddr(addr);
		Word tmp = this.content[ma.row][ma.bank];
		if (ma.byteOrWord == false) // address word
			return tmp.toIntArray();
		else { // ma.byteOrWord == true, address byte
			if (ma.byteAddress == 0)// return first byte
				return tmp.getFirstByte();
			else
				// ma.byteAddress == 1, return second byte
				return tmp.getSecondByte();
		}
	}

	/***
	 * Set the content of memory at addr address. Both parameters are of int[]
	 * type
	 * 
	 * @param content
	 *            : the content you want to put in target memory
	 * 
	 * @param addr
	 *            : address of the target memory
	 */
	public void setContent(int[] addr, int[] content) {
		// TODO: to be tested. This function is copied from getContent with a
		// slight change
		MemAddr ma = new MemAddr(addr);
		// when write to memory location, we only accept 16 bits word operation.
		int[] tmpArray = new int[16];
		for (int i = 0; i < 16; i++)
			tmpArray[i] = content[i];

		// DBG
		// System.out.println(Arrays.toString(tmpArray));

		// when write to memory location, we only accept 16 bits word
		// operation.
		this.content[ma.row][ma.bank] = new Word(tmpArray);

		// DBG
		// System.out.println("content: "
		// + Arrays.toString(this.content[ma.row][ma.bank].content)
		// + " stored in location: " + ma.row + "|" + ma.bank);

	}

	/***
	 * Set the content of memory at addr address. The address parameter is in
	 * Integer type, while the content is in int[] type.
	 * 
	 * @param content
	 *            : the content you want to put in target memory
	 * 
	 * @param addr
	 *            : address of the target memory
	 */
	public void setContent(int addr, int[] content) {
		// TODO: to be tested.
		MemAddr ma = null;
		try {
			int[] tmpArray = new int[16];
			for (int i = 0; i < 16; i++)
				tmpArray[i] = content[i];

			// DBG
			// System.out.println(Arrays.toString(tmpArray));

			int[] tmpAddr = Util.intToBinary(addr, Memory.WORDLENGTH);
			ma = new MemAddr(tmpAddr);
			// when write to memory location, we only accept 16 bits word
			// operation.
			this.content[ma.row][ma.bank] = new Word(tmpArray);

			// DBG
			// System.out.println("content: "
			// + Arrays.toString(this.content[ma.row][ma.bank].content)
			// + " stored in location: " + ma.row + "|" + ma.bank);

		} catch (IntToBinaryException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}
